﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Behaviorize.Reporting;
using Behaviorize.TextFormatting;

namespace Behaviorize
{
	public class Scenario
	{
		private readonly Feature _feature;
		internal readonly MethodBase ScenarioMethod;
		private readonly List<ScenarioVersion> _scenarioVersions = new List<ScenarioVersion>();

		internal string Name { get; set; }

		public Scenario()
		{
			StackFrame callingFrame = new StackTrace(1, false).GetFrame(0);
			ScenarioMethod = callingFrame.GetMethod();
			Type testClassType = ScenarioMethod.DeclaringType;

			if (testClassType == null)
			{
				throw new ScenarioDeclaredOutsideScopeException("A scenario must be declared inside a class");
			}

			Name = TextFormatter.ToText(ScenarioMethod.Name, false);

			_feature = new Feature
				{
					Name = TextFormatter.ToText(testClassType.Name, false),
					Namespace = testClassType.Namespace,
					Assembly = testClassType.Assembly
				};

			var userStoryAttribute = testClassType.GetCustomAttributes(typeof (UserStoryAttribute), false).SingleOrDefault() as UserStoryAttribute;

			if (userStoryAttribute != null
			    && !String.IsNullOrEmpty(userStoryAttribute.AsA)
			    && !String.IsNullOrEmpty(userStoryAttribute.IWant)
			    && !String.IsNullOrEmpty(userStoryAttribute.SoThat))
			{
				_feature.UserStory = new Story(userStoryAttribute.AsA, userStoryAttribute.IWant, userStoryAttribute.SoThat);
			}

			var descriptionAttribute = testClassType.GetCustomAttributes(typeof (FeatureDescriptionAttribute), false).SingleOrDefault() as FeatureDescriptionAttribute;

			if (descriptionAttribute != null)
			{
				_feature.Description = descriptionAttribute.Description;
			}

			ReportMaker.Instance.CreateFeature(_feature);
		}

		public ArrangementStep Given(Action action)
		{
			ScenarioVersion scenarioVersion = CreateScenarioVersion();

			var step = new ArrangementStep(scenarioVersion, action);
			scenarioVersion.ArrangementSteps.Add(step);
			return step;
		}

		public ArrangementStep Given<T1>(Action<T1> action, T1 arg1)
		{
			ScenarioVersion scenarioVersion = CreateScenarioVersion();

			var step = new ArrangementStep(scenarioVersion, action, arg1);
			scenarioVersion.ArrangementSteps.Add(step);
			return step;
		}

		public ArrangementStep Given<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2)
		{
			ScenarioVersion scenarioVersion = CreateScenarioVersion();

			var step = new ArrangementStep(scenarioVersion, action, arg1, arg2);
			scenarioVersion.ArrangementSteps.Add(step);
			return step;
		}

		public ArrangementStep Given<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3)
		{
			ScenarioVersion scenarioVersion = CreateScenarioVersion();

			var step = new ArrangementStep(scenarioVersion, action, arg1, arg2, arg3);
			scenarioVersion.ArrangementSteps.Add(step);
			return step;
		}

		public ArrangementStep Given<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
		{
			ScenarioVersion scenarioVersion = CreateScenarioVersion();

			var step = new ArrangementStep(scenarioVersion, action, arg1, arg2, arg3, arg4);
			scenarioVersion.ArrangementSteps.Add(step);
			return step;
		}

		private ScenarioVersion CreateScenarioVersion()
		{
			var scenarioVersion = new ScenarioVersion(this);
			_scenarioVersions.Add(scenarioVersion);
			return scenarioVersion;
		}

		public void Verify()
		{
			Exception exceptionToThrow = null;
			var consoleWriter = new ScenarioConsoleWriter(_feature, Name);

			foreach (ScenarioVersion scenarioVersion in _scenarioVersions)
			{
				List<Exception> exceptions = scenarioVersion.Verify();

				consoleWriter.AddScenario(scenarioVersion);

				if (exceptions.Count > 0)
				{
					exceptionToThrow = exceptionToThrow ?? exceptions[0];
				}
			}

			ReportMaker.Instance.CreateReportForScenario(_feature, Name, _scenarioVersions);
			consoleWriter.PrintToConsole();

			ThrowException(exceptionToThrow);
		}

		static void PreserveStackTrace(Exception e)
		{
			var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain);
			var mgr = new ObjectManager(null, ctx);
			var si = new SerializationInfo(e.GetType(), new FormatterConverter());

			e.GetObjectData(si, ctx);
			mgr.RegisterObject(e, 1, si); // prepare for SetObjectData
			mgr.DoFixups(); // ObjectManager calls SetObjectData
		}

		private static void ThrowException(Exception exceptionToThrow)
		{
			if (exceptionToThrow != null)
			{
				PreserveStackTrace(exceptionToThrow);

				throw exceptionToThrow;
			}
		}
	}

	public class ScenarioDeclaredOutsideScopeException : Exception
	{
		public ScenarioDeclaredOutsideScopeException(string message) : base(message) { }
	}

	public class UnsupportedTestFrameworkException : Exception
	{
		public UnsupportedTestFrameworkException(string message):base(message){}
	}
}